%include "./boot/include/boot.inc"

section loader vstart=LOADER_BASE_ADDR

jmp loader_start

;该地址为 0x903 --> LOADER_BASE_ADDR + 3 (jmp loader_start 指令长度为3字节)
total_mem_size dd 0  ;存放内存大小, jmp loader_start 指令长度为3字节，所以，内存大小所在的地址为 0x903，后续内存管理是，需要用到

GDT_BASE:
    dd 0x00000000    ;第一个gdt描述符，默认置空，不可用
    dd 0x00000000
CODE_DESC:
    dd 0x0000ffff   ;代码段的低32位，基地址前16位，以及 段界限的低16位
    dd DESC_CODE_HIGH4

DATA_STACK_DESC:
    dd 0x0000ffff   ;数据栈段的低32位，基地址前16位，以及 段界限的低16位
    dd DESC_DATA_HIGH4

VIDEO_DESC:
    dd 0x80000007   ;显存段，低32位，基地是前16位（显存地址为 0xb8000)
                    ;段界限 （0xbffff - 0xb8000）/ 0x1000(即4k) = 7
    dd DESC_VIDEO_HIGH4

GDT_SIZE equ $ - GDT_BASE  ;计算GDT大小
GDT_LIMIT equ  GDT_SIZE - 1 ; 计算GDT界限

times 60 dq 0  ; 预留60个描述符空间  dq--> 写入双字节

SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0  ;代码段描述符，即下标是1的
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

gdtr_prt dw GDT_LIMIT       ;配合 LGDT 加载 GDT
         dd GDT_BASE

loader_msg: db '2 LOADER on Real MODE, Hello World! by kkk'
loader_msg_end:
ards_buf times 244 db 0                                 ;人工对齐total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_nr dw 0

loader_start:
    mov cx,loader_msg_end - loader_msg
    mov bx,loader_msg
    mov si,160
    
.show_loader_msg:
    mov al,[bx]
    mov byte [gs:si], al
    inc si
    mov byte [gs:si],0xa4
    inc si
    add bx,1
    loop .show_loader_msg

.get_mem_size:
      ;-------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------

    xor ebx, ebx		                                ;第一次调用时，ebx值要为0
    mov edx, 0x534d4150	                                ;edx只赋值一次，循环体中不会改变
    mov di, ards_buf	                                ;ards结构缓冲区
    .e820_mem_get_loop:	                                ;循环获取每个ARDS内存范围描述结构
    mov eax, 0x0000e820	                                ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
    mov ecx, 20		                                    ;ARDS地址范围描述符结构大小是20字节
    int 0x15
    add di, cx		                                    ;使di增加20字节指向缓冲区中新的ARDS结构位置
    inc word [ards_nr]	                                ;记录ARDS数量
    cmp ebx, 0		                                    ;若ebx为0且cf不为1,这说明ards全部返回，当前已是最后一个
    jnz .e820_mem_get_loop

                                                        ;在所有ards结构中，找出(base_add_low + length_low)的最大值，即内存的容量。
    mov cx, [ards_nr]	                                ;遍历每一个ARDS结构体,循环次数是ARDS的数量
    mov ebx, ards_buf 
    xor edx, edx		                                ;edx为最大的内存容量,在此先清0
.find_max_mem_area:	                                    ;无须判断type是否为1,最大的内存块一定是可被使用
    mov eax, [ebx]	                                    ;base_add_low
    add eax, [ebx+8]	                                ;length_low
    add ebx, 20		                                    ;指向缓冲区中下一个ARDS结构
    cmp edx, eax		                                ;冒泡排序，找出最大,edx寄存器始终是最大的内存容量
    jge .next_ards
    mov edx, eax		                                ;edx为总内存大小
.next_ards:
    loop .find_max_mem_area

    mov [total_mem_size], edx	                        ;将内存换为byte单位后存入total_mem_size处。


;开始进入保护模式,一下是固定写法，必须这么写
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1

;1 打开A20
in al, 0x92
or al, 0000_0010b
out 0x92,al

; 2 加载GDT
lgdt [gdtr_prt]

;3 将cr0的pe位置1
mov eax , cr0
or eax,0x00000001
mov cr0,eax

jmp SELECTOR_CODE:p_mode_start  ;段间远跳，刷新cpu流水线


[bits 32]
p_mode_start:
    mov ax,SELECTOR_DATA
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov esp,LOADER_STACK_TOP
    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov byte [gs:320],'P'   ;第六行输出一个 P
    call setup_page

    mov eax, [gdtr_prt + 2]
    or dword [eax + 24 + 4],0xc0000000  ;视频段中的基地址 + 3g
    
    add dword [gdtr_prt + 2], 0xc0000000 ;gdt基地址 加3g
    add esp, 0xc0000000 ; esp 加3g

    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax
    mov eax ,cr0
    or eax, 0x80000000
    mov cr0,eax
    lgdt [gdtr_prt]

load_kernel:
;开始初始化内核加载
    ; 从磁盘读取内核文件
    ; eax=起始的LBA扇区号
    ; ebx=数据写入的目标内存地址
    ; ecx=需要读入的扇区数

    mov eax, KERNEL_START_SECTOR
    mov ebx, KERNEL_BIN_BASE_ADDR
    mov ecx, KERNEL_START_COUNT
    
    call rd_disk_m_32
enter_kernel:
    call init_kernel
    mov esp,0xc009f000
    jmp KERNEL_ENTRY_POINT                              ; 用地址0x1500访问测试，结果ok

init_kernel:
    xor eax,eax
    xor ebx,ebx ;program header表地址
    xor ecx,ecx ;program header 数量
    xor edx,edx  ;program header 大小


    mov dx, [KERNEL_BIN_BASE_ADDR + 42] ;每个program header的字节数
    mov ebx,[KERNEL_BIN_BASE_ADDR + 28] ;program header 表的偏移量
    add ebx, KERNEL_BIN_BASE_ADDR  ; 便宜地址加上 KERNEL elf文件的加载地址，得出 program header表的实际地址
    mov cx,[KERNEL_BIN_BASE_ADDR + 44] ;program header 的数量 
   
    .for_each_program_header:
        cmp byte [ebx + 0], PT_NULL
        je .PTNULL
       
        push dword [ebx + 16] ; 第三个参数 代码段的size字节数
        mov eax,[ebx + 4] ;4字节偏移初，为本program header对应的 代码段的便宜量
        add eax , KERNEL_BIN_BASE_ADDR  ; 加上kernel elf 文件的加载地址，得出代码段实际的位置
        push eax ; 第二个参数 复制源地址，src
        push dword [ebx + 8 ] ; 第一个参数 目标地址，dst
  
        call mem_copy ;入参，3个参数放入栈中（dst,src,size）

        add esp,12 ;栈回退3 * 4字节 即，回退3个参数的空间
    .PTNULL:
        add ebx, edx
    loop .for_each_program_header
ret


;1. 清空 页目录表的4k空间
;2. 创建第一页页表项，并存入页目录的第一项，以及第768项（高端的3G空间开始处）
setup_page:
    mov ecx,4096
    xor esi,esi
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS + esi], 0
    inc esi
    loop .clear_page_dir

.create_pde:
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000 
    mov ebx, eax
    or eax, PG_US_U | PG_RW_W | PG_P

    
    mov [PAGE_DIR_TABLE_POS + 0x00],eax
    mov [PAGE_DIR_TABLE_POS + 0xc00],eax   ;第768项的偏移量为 768 * 4字节 = 0x

    sub eax, 0x1000
    mov[PAGE_DIR_TABLE_POS + 4092], eax  ;最后一项存入 页目录本身的地址

    ;开始初始化 0~ 1MB对应的页表
    ;每个页表项对应4KB空间，所以，需要创建 1024 KB / 4KB  =  256个页表项
    
    mov ecx, 256
    mov esi, 0
    xor edx, edx  ; 用edx存放页框（物理页）的起始地址
    mov edx, PG_US_U | PG_RW_W | PG_P	
.create_pte:
    mov [ebx + esi * 4], edx
    add edx,0x1000 ;页框地址加4kb
    inc esi
    loop .create_pte

    ;初始化页目录表的高3G项。
    ;由于768项，以及最后一项已经在create_pde中完成初始化
    ;以下代码从 769项开始，初始化 254项（256 - 1（768项） - 1 （最后一项））

    mov ecx, 254
    mov esi, 769
    mov ebx, PAGE_DIR_TABLE_POS
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000
    or eax , PG_US_U | PG_RW_W | PG_P
.create_kernal_pde:
    mov[ebx + esi * 4], eax
    inc esi
    add eax, 0x1000
    loop .create_kernal_pde
ret


;入参，3个参数放入栈中（dst,src,size）
mem_copy:
    cld
    push ebp
    mov ebp, esp
    push ecx

    mov edi,[ebp + 8] ;由于函数call进入时，会自动压入返回地址，所以，这里是 ebp + 4 + 4获取到第一个参数
    mov esi,[ebp + 12]
    mov ecx,[ebp + 16]
    rep movsb
    pop ecx
    pop ebp
ret

; 从硬盘读取n个扇区
; eax=起始的LBA扇区号
; ebx=数据写入的目标内存地址
; ecx=需要读入的扇区数
rd_disk_m_32:
	mov esi,eax
	mov di,cx

	mov dx,0x1f2 ;端口号 0x1f2，主通道读取时，用于指定读取的扇区数
	mov al,cl
	out dx,al

	mov eax,esi ; 恢复 eax

	mov dx,0x1f3 ; 指定起始扇区号的 LBA底8位
	out dx,al

	mov cl,8
	shr eax,cl
	mov dx,0x1f4 ; 指定起始扇区号的 LBA的中间8位
	out dx,al

	shr eax,cl
	mov dx, 0x1f5 ;指定起始扇区号的 LBA高8位
	out dx,al

	shr eax,cl
	and al,0x0f ; lba的 24 - 27位
	or al,0xe0  ;设置 al的高4位为 1110，表示lba模式
	mov dx,0x1f6
	out dx,al

	mov dx,0x1f7	;往 0x1f7端口写入 0x20后，硬盘开始读取
	mov al,0x20
	out dx,al		;此时，硬盘开始工作

;开始检测硬盘状态
.not_ready:		
	nop
	in al,dx	;依旧使用 0x1f7端口，可以获取到硬盘的工作状态
	and al,1000_1000b ;第4位为1 表示硬盘控制器已经准备好数据传输，第8位为1，表示硬盘忙碌
	cmp al,0000_1000b ; 预期结果为第4位为1（准备好），第8位为0（不忙碌）
	jnz .not_ready	;不满足上述条件时，继续等待

;开始读取数据
	;计算需要读取的次数，
	mov ax,di	;读取的扇区数
	mov dx,256
	mul dx ;每次可以读取2个字节，每个扇区为512字节，所以这里乘上 256
			   ;得到了需要读取的次数
	mov cx,ax  ;将读取次数放入到loop的循环次数寄存器 cx

	mov dx,0x1f0 ;从 0x1f0端口读取数据
.go_on_read:
	in ax,dx
	mov [ebx],ax
	add ebx,2
	loop .go_on_read
	ret